在 vue 实例调用 mountComponent 挂载时,会实例化一个渲染 Watcher。当响应式数据更新时,通知渲染 Watcher,调用回调函数 _update。
function mountComponent (
vm,
el,
hydrating
) {
vm.$el = el;
// ...
var updateComponent;
/* istanbul ignore if */
if (config.performance && mark) {
updateComponent = function () {
// ...
vm._update(vnode, hydrating); // 调用 _update 函数执行 __patch__
// ...
};
} else {
updateComponent = function () {
vm._update(vm._render(), hydrating); // 调用 _update 函数执行 __patch__
};
}
// 渲染 Watcher
new Watcher(vm, updateComponent, noop, {
before: function before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
hydrating = false;
// ...
return vm
}
_update 函数在 lifecycleMixin 时已挂载到 Vue 原型上,在 _update 函数内部会调用 __patch__ 对元素节点进行更新。
function lifecycleMixin (Vue) {
Vue.prototype._update = function (vnode, hydrating) {
var vm = this;
var prevEl = vm.$el;
var prevVnode = vm._vnode;
var restoreActiveInstance = setActiveInstance(vm);
vm._vnode = vnode;
// 调用 __patch__ 更新元素节点
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode);
}
restoreActiveInstance();
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null;
}
if (vm.$el) {
vm.$el.__vue__ = vm;
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el;
}
};
}
__patch__ 函数挂载在 Vue 原型上,并且只有在浏览器端才会有 patch 逻辑,因为 patch 最终更新的是实际的 DOM 节点。
Vue.prototype.__patch__ = inBrowser ? patch : noop;
var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules });
function createPatchFunction (backend) {
// ...
return function patch(oldVnode, vnode, hydrating, removeOnly) {
// ...
return vnode.elm
}
}
由以上源码可知,__patch__ 函数最终调用的是 createPatchFunction 函数返回的 patch 函数。
# patch 函数
patch 函数有四个参数:
- oldVnode:旧 vnode 数据
- vnode:新 vnode 数据
- hydrating:是否为服务端渲染。
- removeOnly:是否只执行移除逻辑。
函数源码:
function patch(oldVnode, vnode, hydrating, removeOnly) {
// 新 vnode 为空,说明该 vnode 已删除,调用 oldVnode 的 destroy hook
if (isUndef(vnode)) {
if (isDef(oldVnode)) { invokeDestroyHook(oldVnode); }
return
}
var isInitialPatch = false;
var insertedVnodeQueue = [];
if (isUndef(oldVnode)) {
// 旧 vnode 为空,直接根据新 vnode 创建元素节点
isInitialPatch = true;
createElm(vnode, insertedVnodeQueue);
} else {
var isRealElement = isDef(oldVnode.nodeType);
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 通过 sameVnode 判断新旧 vnode 为相同节点,调用 patchVnode 更新旧 Vnode
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
} else {
if (isRealElement) {
// 判断是否为服务端渲染
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR);
hydrating = true;
}
if (isTrue(hydrating)) {
// 服务端渲染客户端激活逻辑
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { // 客户端激活
invokeInsertHook(vnode, insertedVnodeQueue, true); // 执行客户端相关逻辑,如事件绑定等
return oldVnode
} else {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.'
);
}
}
// 如果不是服务端渲染,或者客户端激活失败,则根据旧 Vnode 创建空 Vnode
oldVnode = emptyNodeAt(oldVnode);
}
// 旧节点更新完成之后,替换元素节点
var oldElm = oldVnode.elm;
var parentElm = nodeOps.parentNode(oldElm);
// 创建新节点
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
);
// 递归更新父节点的占位节点
if (isDef(vnode.parent)) {
var ancestor = vnode.parent;
var patchable = isPatchable(vnode);
while (ancestor) {
for (var i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor);
}
ancestor.elm = vnode.elm;
if (patchable) {
for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
cbs.create[i$1](emptyNode, ancestor);
}
// #6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
var insert = ancestor.data.hook.insert;
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (var i$2 = 1; i$2 < insert.fns.length; i$2++) {
insert.fns[i$2]();
}
}
} else {
registerRef(ancestor);
}
ancestor = ancestor.parent;
}
}
// 没有父节点,销毁旧节点
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0);
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode);
}
}
}
// 调用 invokeInsertHook
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
return vnode.elm
}
执行逻辑:
- 首先判断如果新旧 vnode 是否为空:
- 如果新 vnode 为空,说明该 vnode 已删除,调用 oldVnode 的 destroy hook。
- 如果旧 vnode 为空,直接根据新 vnode 创建元素节点。
- 接下来判断旧 vnode 是否为真实的 DOM 节点:
- 如果不是,说明旧 vnode 是 vnode 数据;接着通过 sameVnode 函数判断新旧 vnode 是否相同,如果相同则调用 patchVnode 函数进行更新。
- 如果是,判断是否为服务端渲染,如果是则执行客户端激活逻辑。调用 hydrate 函数对实际的节点进行更新和调用 invokeInsertHook 激活客户端代码,如事件绑定等。
# patchVnode 函数
函数参数为:
- oldVnode: 旧 vnode。
- vnode:新 vnode。
- insertedVnodeQueue:待插入 vnode 队列。
- ownerArray:源 vnode 数据,用于复用克隆 vnode 使用。
- index:当前 vnode 索引。
- removeOnly:是否只执行移除逻辑。
函数源码:
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
// 新旧 vnode 相同,直接返回
if (oldVnode === vnode) {
return
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode);
}
var elm = vnode.elm = oldVnode.elm;
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue);
} else {
vnode.isAsyncPlaceholder = true;
}
return
}
// 复用静态节点
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance;
return
}
var i;
var data = vnode.data;
// 调用 prepacth,更新子组件的 propsData、listeners,触发子组件 render-watcher 的 setter,进而更新子组件
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode);
}
var oldCh = oldVnode.children;
var ch = vnode.children;
if (isDef(data) && isPatchable(vnode)) {
// 执行 cbs.update 中的所有函数
for (i = 0; i < cbs.update.length; ++i) { cbs.update[i](oldVnode, vnode); }
if (isDef(i = data.hook) && isDef(i = i.update)) { i(oldVnode, vnode); }
}
if (isUndef(vnode.text)) {
// 非文本节点
if (isDef(oldCh) && isDef(ch)) {
// 新旧 vnode 都有子节点,执行 updateChildren 更新子节点
if (oldCh !== ch) { updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); }
} else if (isDef(ch)) {
// 新 vnode 有子节点而旧 vnode 没有子节点,检验 key 值后将新 vnode 的子节点添加到插入队列 insertedVnodeQueue 中
{
checkDuplicateKeys(ch);
}
if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, ''); }
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) {
// 旧 vnode 有子节点而新 vnode 没有子节点,直接删除旧 vnode 的子节点
removeVnodes(oldCh, 0, oldCh.length - 1);
} else if (isDef(oldVnode.text)) {
// 新旧 vnode 都有没有子节点,但是旧 vnode 有文本,则置空文本
nodeOps.setTextContent(elm, '');
}
} else if (oldVnode.text !== vnode.text) {
// 是文本节点且新旧 vnode 的文本内容不相同,则直接替换实际元素节点上的文本内容
nodeOps.setTextContent(elm, vnode.text);
}
if (isDef(data)) {
// 调用 postpatch 函数
if (isDef(i = data.hook) && isDef(i = i.postpatch)) { i(oldVnode, vnode); }
}
}
函数执行逻辑如下:
判断新旧 vnode 是否相同,相同的话直接返回。
判断新旧 vnode 是否为静态节点,是的话复用静态节点。
调用 prepacth,更新子组件的 propsData、listeners,触发子组件 render-watcher 的 setter,触发子组件更新逻辑,即:触发子组件的 _update 函数。
执行 cbs.update 中的所有函数。
判断新 vnode 是否为文本节点,如果非文本节点:
- 新旧 vnode 都有子节点,执行 updateChildren 更新子节点,也就是 diff patch 的执行逻辑。
- 新 vnode 有子节点而旧 vnode 没有子节点,检验 key 值后将新 vnode 的子节点添加到插入队列 insertedVnodeQueue 中。
- 旧 vnode 有子节点而新 vnode 没有子节点,直接删除旧 vnode 的子节点。
- 新旧 vnode 都有没有子节点,但是旧 vnode 有文本,则置空文本。
判断新 vnode 是否为文本节点,如果是文本节点且新旧 vnode 的文本内容不相同,则直接替换实际元素节点上的文本内容
执行 patch 逻辑完成之后的 postpatch 函数。
以上就是 patchVnode 的执行逻辑,接下来看 updateChildren 函数,也就是 diff patch 的执行逻辑。
# updateChildren 函数
函数参数如下:
- parentElm:父 DOM 元素。
- oldCh:旧子列表。
- newCh:新子列表。
- insertedVnodeQueue:待插入 vnode 队列。
- removeOnly:是否只执行移除逻辑。
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
var oldStartIdx = 0; // 旧子 vnode 列表头指针
var newStartIdx = 0; // 新子 vnode 列表头指针
var oldEndIdx = oldCh.length - 1; // 旧子 vnode 列表尾指针
var oldStartVnode = oldCh[0]; // 旧子 vnode 列表首元素
var oldEndVnode = oldCh[oldEndIdx]; // 旧子 vnode 列表尾元素
var newEndIdx = newCh.length - 1; // 新子 vnode 列表尾指针
var newStartVnode = newCh[0]; // 新子 vnode 列表首元素
var newEndVnode = newCh[newEndIdx]; // 新子 vnode 列表首元素
var oldKeyToIdx, // Map 数据结构,key-index 用于存放旧 vnode 的 key 和 vnode-index 的映射关系,用于判断新 vnode 是否可以复用
idxInOld, // 存放查找新 vnode 在旧 vnode 列表中的索引
vnodeToMove,
refElm;
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
var canMove = !removeOnly;
{
// 校验 key 值
checkDuplicateKeys(newCh);
}
// 遍历新旧子 vnode 列表,直到其中一个遍历结束为止
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 头尾指针双端两两比较,各个情况如下:
if (isUndef(oldStartVnode)) {
// 向右查找旧子 vnode 列表第一个有 vnode 数据的节点(首元素)
oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
// 向左查找旧子 vnode 列表第一个有 vnode 数据的节点(尾元素)
oldEndVnode = oldCh[--oldEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 新旧子 vnode 列表各自的头指针指向的 vnode(首元素)相同,调用 patchVnode 函数更新
// 旧头指针右移
// 新头指针右移
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 新旧子 vnode 列表各自的尾指针指向的 vnode(尾元素)相同,调用 patchVnode 函数更新
// 旧尾指针左移
// 新尾指针左移
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// 旧子 vnode 列表的首元素 和 新子 vnode 列表的尾元素相同,说明该 vnode 被移动到右边
// 调用 patchVnode 更新
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
// 在旧 vnode 的尾元素所在的实际 DOM 元素后面插入更新后的旧 vnode 所在的实际 DOM 元素
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
// 旧头指针右移
oldStartVnode = oldCh[++oldStartIdx];
// 新尾指针左移
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// 旧子 vnode 列表的尾元素 和 新子 vnode 列表的首元素相同,说明该 vnode 被移动到左边
// 调用 patchVnode 更新
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
// 在旧 vnode 的头元素所在的实际 DOM 元素前面插入更新后的旧 vnode 所在的实际 DOM 元素
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
// 旧尾指针左移
oldEndVnode = oldCh[--oldEndIdx];
// 新头指针右移
newStartVnode = newCh[++newStartIdx];
} else {
// 头尾指针双端两两比较之后,其他情况如下:
// 如果没有旧 vnode 的 key-index Map,则调用 createKeyToOldIdx 创建该 Map
if (isUndef(oldKeyToIdx)) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); }
// 通过新 vnode 的 key 值在 oldKeyToIdx 中查找是否在旧 vnode 中存在该 vnode
// 查找是否在旧 vnode 中存在该 vnode
// 1. 在 oldKeyToIdx 中查找
// 2. 在 oldKeyToIdx 中查找不到,则再在旧 vnode 列表中遍历查找
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
if (isUndef(idxInOld)) {
// 旧 vnode 列表中无当前新 vnode,创建新 DOM 节点并添加到旧头元素所在的实际 DOM 元素之前
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
} else {
// 旧 vnode 列表中存在当前新 vnode,说明节点被移动了
vnodeToMove = oldCh[idxInOld];
// 判断是否为相同 vnode
if (sameVnode(vnodeToMove, newStartVnode)) {
// 新旧 vnode 为相同 vnode,则调用 patchVnode 函数更新 oldVnode
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
oldCh[idxInOld] = undefined; // 将移动前的旧 vnode 设置为 undefined
// 在旧首元素之前插入更新后的 oldVnode 实际节点
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
} else {
// same key but different element. treat as new element
// 相同 key 值但是新旧 vnode 为不同 vnode,创建新 DOM 节点并添加到旧头元素所在的实际 DOM 元素之前
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
}
}
// 新头指针右移
newStartVnode = newCh[++newStartIdx];
}
}
if (oldStartIdx > oldEndIdx) {
// 旧头指针移动到旧尾指针后面,说明旧子 vnode 列表已经遍历结束,且新子 vnode 列表长度大于旧子 vnode 列表
// 将新子 vnode 列表中剩下的 vnode 添加到新尾元素所在的实际 DOM 元素之后
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
} else if (newStartIdx > newEndIdx) {
// 新头指针移动到新尾指针后面,说明新子 vnode 列表已经遍历结束,且旧子 vnode 列表长度大于旧子 vnode 列表
// 移除旧子 vnode 列表中剩下的 vnode
removeVnodes(oldCh, oldStartIdx, oldEndIdx);
}
}
updateChildren 的核心逻辑为:
- 采用双指针法,对新旧 vnode 列表进行双端两两比较,即:新首旧首、新尾旧尾、新首旧尾、新尾旧首。
- 在进行双端两两比较之后,其他情况则会创建旧 vnode 列表的 Map 数据结构 oldKeyToIdx,用于存放 vnode 的 key-index 的映射关系。然后将新 vnode 的 key 在 oldKeyToIdx 中查找,找不到则再在旧 vnode 列表中查找,最终找得到则判断是否相同 vnode,是的话则在旧首元素之前插入节点;在 oldKeyToIdx 中找不到或者找得到但新旧 vnode 不为相同 vnode,则都直接创建新的元素节点。
- 当新旧 vnode 的 while 循环遍历结束:
- 如果旧头指针移动到旧尾指针后面,说明旧子 vnode 列表已经遍历结束,且新子 vnode 列表长度大于旧子 vnode 列表,就需要将新子 vnode 列表中剩下的 vnode 添加到新尾元素所在的实际 DOM 元素之后。
- 如果新头指针移动到新尾指针后面,说明新子 vnode 列表已经遍历结束,且旧子 vnode 列表长度大于旧子 vnode 列表,则需要移除旧子 vnode 列表中剩下的 vnode。
另外,由于在通过 sameVnode 判断为同一 vnode 后,会对递归调用 patchVnode 函数。所以该算法是深度优先遍历,找到同层 vnode 列表再进行 diff。
# sameVnode 函数
判断两个 vnode 是否相同的源码如下:
function sameVnode (a, b) {
return (
a.key === b.key &&
a.asyncFactory === b.asyncFactory && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
isUndef(b.asyncFactory.error)
)
)
)
}
# 举个例子
下面为开始数据状态,DOM 和旧 Vnode 列表相同。
第一次,oldStartVnode 与 newEndVnode 相同,则:在实际 DOM 中将 oldStartIdx 的节点移动到 oldEndIdx 的节点的右边,同时,oldStartIdx 右移,newEndIdx 左移。
第二次,oldEndVnode 与 newStartVnode 相同,则:在实际 DOM 中将 oldEndIdx 的节点移动到 oldStartIdx 的节点的左边,同时,oldEndIdx 左移,newStartIdx 右移。
第三次,oldStartVnode 与 newStartVnode 相同,oldStartIdx 右移,newStartIdx 右移。
第四次,newStartVnode 存在于旧 vnode 列表中,在实际 DOM 中将 newStartIdx 的节点移动到旧头指针实际节点之前,并将旧列表中该 vnode 设置为 undefined,同时 newStartIdx 右移。
第五次,oldEndVnode 与 newStartVnode 相同,则:在实际 DOM 中将 oldEndIdx 的节点移动到 oldStartIdx 的节点的左边,同时,oldEndIdx 左移,newStartIdx 右移。
第六次,新 startVnode 不在旧 vnode 列表中,则将该节点插入到旧头元素代表的实际节点的前面。
第七次,newStartIdx > newEndIdx,说明旧 vnode 列表长度大于新 vnode 列表长度,直接将旧 vnode 列表剩余节点在实际 DOM 中存在的节点删掉。